#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include <sys/socket.h>

#include "httpd.h"

#define LS_ALLOC_SIZE (4 * 4096)
#define MAX_CACHE_AGE   3600   /* seconds */
#define HOMEPAGE "https://code.google.com/p/gx-gongxiang/"
#define CACHE_SIZE 32

static pthread_mutex_t lock_dircache = PTHREAD_MUTEX_INITIALIZER;

static char *xgetpwuid(uid_t uid)
{
    static char           *cache[CACHE_SIZE];
    static uid_t          uids[CACHE_SIZE];
    static unsigned int   used, next;
    struct passwd *pw;
    int i;

    for (i = 0; i < used; i++) {
        if (uids[i] == uid) {
            return cache[i];
        }
    }

    /* 404 */
    pw = getpwuid(uid);

    if (NULL != cache[next]) {
        free(cache[next]);
        cache[next] = NULL;
    }

    if (NULL != pw) {
        cache[next] = strdup(pw->pw_name);
    }

    uids[next] = uid;
    next++;

    if (CACHE_SIZE == next) {
        next = 0;
    }

    if (used < CACHE_SIZE) {
        used++;
    }

    return pw ? pw->pw_name : NULL;
}

static char *xgetgrgid(gid_t gid)
{
    static char           *cache[CACHE_SIZE];
    static gid_t          gids[CACHE_SIZE];
    static unsigned int   used, next;
    struct group *gr;
    int i;

    for (i = 0; i < used; i++) {
        if (gids[i] == gid) {
            return cache[i];
        }
    }

    /* 404 */
    gr = getgrgid(gid);

    if (NULL != cache[next]) {
        free(cache[next]);
        cache[next] = NULL;
    }

    if (NULL != gr) {
        cache[next] = strdup(gr->gr_name);
    }

    gids[next] = gid;
    next++;

    if (CACHE_SIZE == next) {
        next = 0;
    }

    if (used < CACHE_SIZE) {
        used++;
    }

    return gr ? gr->gr_name : NULL;
}

struct myfile {
    int           r;
    struct stat   s;
    char          n[1];
};

static int compare_files(const void *a, const void *b)
{
    const struct myfile *aa = *(struct myfile **)a;
    const struct myfile *bb = *(struct myfile **)b;

    if (S_ISDIR(aa->s.st_mode) !=  S_ISDIR(bb->s.st_mode)) {
        return S_ISDIR(aa->s.st_mode) ? -1 : 1;
    }

    return strcmp(aa->n, bb->n);
}

static char do_quote[256];

void init_quote(void)
{
    int i;

    for (i = 0; i < 256; i++) {
        do_quote[i] = (isalnum(i) || ispunct(i)) ? 0 : 1;
    }

    do_quote['+'] = 1;
    do_quote['#'] = 1;
    do_quote['%'] = 1;
    do_quote['"'] = 1;
    do_quote['?'] = 1;
}

char *quote(unsigned char *path, int maxlength)
{
    static char buf[2048];
    int i, j, n = strlen((char *)path);

    if (n > maxlength) {
        n = maxlength;
    }

    for (i = 0, j = 0; i < n && j < sizeof(buf) - 4; i++, j++) {
        if (!do_quote[path[i]]) {
            buf[j] = path[i];
            continue;
        }

        sprintf(buf + j, "%%%02x", path[i]);
        j += 2;
    }

    buf[j] = 0;
    return buf;
}

static void strmode(mode_t mode, char *dest)
{
    static const char *rwx[] = {
        "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"
    };

    /* file type */
    switch (mode & S_IFMT) {
        case S_IFIFO:
            dest[0] = 'p';
            break;
        case S_IFCHR:
            dest[0] = 'c';
            break;
        case S_IFDIR:
            dest[0] = 'd';
            break;
        case S_IFBLK:
            dest[0] = 'b';
            break;
        case S_IFREG:
            dest[0] = '-';
            break;
        case S_IFLNK:
            dest[0] = 'l';
            break;
        case S_IFSOCK:
            dest[0] = '=';
            break;
        default:
            dest[0] = '?';
            break;
    }

    /* access rights */
    sprintf(dest + 1, "%s%s%s",
            rwx[(mode >> 6) & 0x7],
            rwx[(mode >> 3) & 0x7],
            rwx[mode        & 0x7]);
}

static char *ls(time_t now, char *hostname, char *filename, char *path, int *length)
{
    DIR            *dir;
    struct dirent  *file;
    struct myfile  **files = NULL;
    struct myfile  **re1;
    char           *h1, *h2, *re2, *buf = NULL;
    int            count, len, size, i;
    uid_t          uid;
    gid_t          gid;
    char           line[1024];
    char           *pw = NULL, *gr = NULL;

    if (NULL == (dir = opendir(filename))) {
        return NULL;
    }

    /* read dir */
    uid = getuid();
    gid = getgid();

    for (count = 0;; count++) {
        if (NULL == (file = readdir(dir))) {
            break;
        }

        if (0 == strcmp(file->d_name, ".")) {
            /* skip the the "." directory */
            count--;
            continue;
        }

        if (0 == strcmp(path, "/") && 0 == strcmp(file->d_name, "..")) {
            /* skip the ".." directory in root dir */
            count--;
            continue;
        }

        if (0 == (count % 64)) {
            re1 = realloc(files, (count + 64) * sizeof(struct myfile *));

            if (NULL == re1) {
                goto oom;
            }

            files = re1;
        }

        files[count] = malloc(strlen(file->d_name) + sizeof(struct myfile));

        if (NULL == files[count]) {
            goto oom;
        }

        strcpy(files[count]->n, file->d_name);
        sprintf(line, "%s/%s", filename, file->d_name);

        if (-1 == stat(line, &files[count]->s)) {
            free(files[count]);
            count--;
            continue;
        }

        files[count]->r = 0;

        if (S_ISDIR(files[count]->s.st_mode) || S_ISREG(files[count]->s.st_mode)) {
            if (files[count]->s.st_uid == uid && files[count]->s.st_mode & 0400) {
                files[count]->r = 1;
            } else if (files[count]->s.st_gid == gid && files[count]->s.st_mode & 0040) {
                files[count]->r = 1;
            } else if (files[count]->s.st_mode & 0004) {
                files[count]->r = 1;
            }
        }
    }

    closedir(dir);

    /* sort */
    if (count) {
        qsort(files, count, sizeof(struct myfile *), compare_files);
    }

    /* output */
    size = LS_ALLOC_SIZE;
    buf  = malloc(size);

    if (NULL == buf) {
        goto oom;
    }

    len  = 0;
    len += sprintf(buf + len,
                   "<head><meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\"><title>%s:%d%s</title></head>\n"
                   "<body bgcolor=white text=black link=darkblue vlink=firebrick>\n"
                   "<h1>listing: \n",
                   hostname, tcp_port, path);
    h1 = path, h2 = path + 1;

    for (;;) {
        if (len > size) {
            abort();
        }

        if (len + (LS_ALLOC_SIZE >> 2) > size) {
            size += LS_ALLOC_SIZE;
            re2 = realloc(buf, size);

            if (NULL == re2) {
                goto oom;
            }

            buf = re2;
        }

        len += sprintf(buf + len, "<a href=\"%s\">%*.*s</a>",
                       quote((unsigned char *) path, h2 - path),
                       (int)(h2 - h1),
                       (int)(h2 - h1),
                       h1);
        h1 = h2;
        h2 = strchr(h2, '/');

        if (NULL == h2) {
            break;
        }

        h2++;
    }

    len += sprintf(buf + len,
                   "</h1><hr noshade size=1><pre>\n"
                   "<b>access      user      group     date             "
                   "size  name</b>\n\n");

    for (i = 0; i < count; i++) {
        if (len > size) {
            abort();
        }

        if (len + (LS_ALLOC_SIZE >> 2) > size) {
            size += LS_ALLOC_SIZE;
            re2 = realloc(buf, size);

            if (NULL == re2) {
                goto oom;
            }

            buf = re2;
        }

        /* mode */
        strmode(files[i]->s.st_mode, buf + len);
        len += 10;
        buf[len++] = ' ';
        buf[len++] = ' ';
        /* user */
        pw = xgetpwuid(files[i]->s.st_uid);

        if (NULL != pw) {
            len += sprintf(buf + len, "%-8.8s  ", pw);
        } else {
            len += sprintf(buf + len, "%8d  ", (int)files[i]->s.st_uid);
        }

        /* group */
        gr = xgetgrgid(files[i]->s.st_gid);

        if (NULL != gr) {
            len += sprintf(buf + len, "%-8.8s  ", gr);
        } else {
            len += sprintf(buf + len, "%8d  ", (int)files[i]->s.st_gid);
        }

        /* mtime */
        if (now - files[i]->s.st_mtime > 60 * 60 * 24 * 30 * 6)
            len += strftime(buf + len, 255, "%b %d  %Y  ",
                            gmtime(&files[i]->s.st_mtime));
        else
            len += strftime(buf + len, 255, "%b %d %H:%M  ",
                            gmtime(&files[i]->s.st_mtime));

        /* size */
        if (S_ISDIR(files[i]->s.st_mode)) {
            len += sprintf(buf + len, "  &lt;DIR&gt;  ");
        } else if (!S_ISREG(files[i]->s.st_mode)) {
            len += sprintf(buf + len, "     --  ");
        } else if (files[i]->s.st_size < 1024 * 9) {
            len += sprintf(buf + len, "%4d  B  ",
                           (int)files[i]->s.st_size);
        } else if (files[i]->s.st_size < 1024 * 1024 * 9) {
            len += sprintf(buf + len, "%4d kB  ",
                           (int)(files[i]->s.st_size >> 10));
        } else if ((int64_t)(files[i]->s.st_size) < (int64_t)1024 * 1024 * 1024 * 9) {
            len += sprintf(buf + len, "%4d MB  ",
                           (int)(files[i]->s.st_size >> 20));
        } else if ((int64_t)(files[i]->s.st_size) < (int64_t)1024 * 1024 * 1024 * 1024 * 9) {
            len += sprintf(buf + len, "%4d GB  ",
                           (int)(files[i]->s.st_size >> 30));
        } else {
            len += sprintf(buf + len, "%4d TB  ",
                           (int)(files[i]->s.st_size >> 40));
        }

        /* filename */
        if (files[i]->r) {
            len += sprintf(buf + len, "<a href=\"%s%s\">%s</a>\n",
                           quote((unsigned char *) files[i]->n, 9999),
                           S_ISDIR(files[i]->s.st_mode) ? "/" : "",
                           files[i]->n);
        } else {
            len += sprintf(buf + len, "%s\n", files[i]->n);
        }
    }

    strftime(line, 32, "%d/%b/%Y %H:%M:%S GMT", gmtime(&now));
    len += sprintf(buf + len,
                   "</pre><hr noshade size=1>\n"
                   "<small><a href=\"%s\">%s</a> &nbsp; %s</small>\n"
                   "</body>\n",
                   HOMEPAGE, server_name, line);

    for (i = 0; i < count; i++) {
        free(files[i]);
    }

    if (count) {
        free(files);
    }

    /* return results */
    *length = len;
    return buf;
oom:
    fprintf(stderr, "oom\n");

    if (files) {
        for (i = 0; i < count; i++)
            if (files[i]) {
                free(files[i]);
            }

        free(files);
    }

    if (buf) {
        free(buf);
    }

    return NULL;
}

struct DIRCACHE *dirs = NULL;

void free_dir(struct DIRCACHE *dir)
{
    DO_LOCK(dir->lock_refcount);
    dir->refcount--;

    if (dir->refcount > 0) {
        DO_UNLOCK(dir->lock_refcount);
        return;
    }

    DO_UNLOCK(dir->lock_refcount);
    FREE_LOCK(dir->lock_refcount);
    FREE_LOCK(dir->lock_reading);
    FREE_COND(dir->wait_reading);

    if (NULL != dir->html) {
        free(dir->html);
    }

    free(dir);
}

struct DIRCACHE *get_dir(struct REQUEST *req, char *filename) {
    struct DIRCACHE  *this, *prev;
    int              i;

    DO_LOCK(lock_dircache);

    for (prev = NULL, this = dirs, i = 0; this != NULL;
         prev = this, this = this->next, i++) {
        if (0 == strcmp(filename, this->path)) {
            /* remove from list */
            if (NULL == prev) {
                dirs = this->next;
            } else {
                prev->next = this->next;
            }

            break;
        }

        if (i > max_dircache) {
            free_dir(this);
            this = NULL;
            prev->next = NULL;
            break;
        }
    }

    if (this) {
        /* check mtime and cache entry age */
        if (now - this->add > MAX_CACHE_AGE || 0 != strcmp(this->mtime, req->mtime)) {
            free_dir(this);
            this = NULL;
        }
    }

    if (!this) {
        /* add a new cache entry to the list */
        this = malloc(sizeof(struct DIRCACHE));
        this->refcount = 2;
        this->reading = 1;
        INIT_LOCK(this->lock_refcount);
        INIT_LOCK(this->lock_reading);
        INIT_COND(this->wait_reading);
        this->next = dirs;
        dirs = this;
        DO_UNLOCK(lock_dircache);
        strcpy(this->path,  filename);
        strcpy(this->mtime, req->mtime);
        this->add   = now;
        this->html  = ls(now, req->hostname, filename, req->path, &(this->length));
        DO_LOCK(this->lock_reading);
        this->reading = 0;
        BCAST_COND(this->wait_reading);
        DO_UNLOCK(this->lock_reading);
    } else {
        /* add back to the list */
        this->next = dirs;
        dirs = this;
        this->refcount++;
        DO_UNLOCK(lock_dircache);
        DO_LOCK(this->lock_reading);

        if (this->reading) {
            WAIT_COND(this->wait_reading, this->lock_reading);
        }

        DO_UNLOCK(this->lock_reading);
    }

    req->body  = this->html;
    req->lbody = this->length;
    return this;
}
